<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Go OTel 应用逻辑解释 - 主程序流程</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
<h1>主程序流程 (<code>main</code> 函数)</h1>

<nav>
    <ul>
        <li><a href="index.html">概述</a></li>
        <li><a href="setup.html">初始化设置</a></li>
        <li><a href="handlers.html">HTTP 处理器</a></li>
        <li><a href="services.html">业务逻辑服务</a></li>
        <li><a href="main_flow.html" class="active">主程序流程</a></li>
        <li><a href="glossary.html">名词解释</a></li>
    </ul>
</nav>

<section>
    <h2>引言</h2>
    <p>
        <code>main</code> 函数是 Go 应用程序的入口点。它负责编排整个应用的启动、运行和关闭过程。这包括初始化依赖项、设置 Web 服务器、启动服务监听，以及处理操作系统的终止信号以实现<a href="glossary.html#graceful-shutdown">优雅关闭</a>。
    </p>
</section>

<section id="main-logic">
    <h2><code>main</code> 函数逻辑</h2>
    <pre><code class="language-go">func main() {
    log.SetFlags(log.LstdFlags | log.Lshortfile) // 配置日志格式
    log.Println("应用程序启动中...")

    <span class="code-comment">// --- 1. 初始化 OpenTelemetry ---</span>
    log.Println("正在初始化 OpenTelemetry...")
    shutdown, err := initTracerProvider() <span class="code-comment">// 调用初始化函数</span>
    if err != nil { log.Fatalf("致命错误: 初始化 TracerProvider 失败: %v", err) }
    <span class="code-comment">// 使用 defer 确保 OTel 关闭函数在 main 退出时被调用</span>
    defer func() {
        ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
        defer cancel()
        log.Println("正在关闭 OpenTelemetry TracerProvider...")
        if err := shutdown(ctx); err != nil { /* log error */ } else { /* log success */ }
    }()
    tracer = otel.Tracer("gin-gorm-mysql-redis-app/main") <span class="code-comment">// 获取全局 Tracer</span>
    log.Println("OpenTelemetry 初始化完成。")

    <span class="code-comment">// --- 2. 初始化数据库 ---</span>
    log.Println("正在初始化数据库连接...")
    db, err = initDB() <span class="code-comment">// 调用数据库初始化</span>
    if err != nil { log.Fatalf("致命错误: 初始化数据库失败: %v", err) }
    sqlDB, err := db.DB() <span class="code-comment">// 获取底层 *sql.DB 用于配置连接池</span>
    if err == nil {
        log.Println("正在配置数据库连接池...")
        sqlDB.SetMaxIdleConns(10)
        sqlDB.SetMaxOpenConns(100)
        sqlDB.SetConnMaxLifetime(time.Hour)
        // ... PING 检查 ...
    } else { /* log warning */ }
    log.Println("数据库初始化完成。")

    <span class="code-comment">// --- 3. 初始化 Redis ---</span>
    log.Println("正在初始化 Redis 连接...")
    rdb, err = initRedis() <span class="code-comment">// 调用 Redis 初始化</span>
    if err != nil { log.Fatalf("致命错误: 初始化 Redis 失败: %v", err) }
    <span class="code-comment">// 使用 defer 确保 Redis 客户端关闭</span>
    defer func() {
        log.Println("正在关闭 Redis 连接...")
        if err := rdb.Close(); err != nil { /* log error */ } else { /* log success */ }
    }()
    log.Println("Redis 初始化完成。")

    <span class="code-comment">// --- 4. 初始化 Gin 引擎 ---</span>
    log.Println("正在初始化 Gin Web 引擎...")
    router := gin.New() <span class="code-comment">// 创建 Gin 实例</span>
    <span class="code-comment">// **关键:** 添加 OTel Gin 中间件，应尽早添加</span>
    router.Use(otelgin.Middleware("gin-server")) <span class="code-comment">// "gin-server" 是 HTTP 服务名</span>
    router.Use(gin.Logger())                    <span class="code-comment">// Gin 日志中间件</span>
    router.Use(gin.Recovery())                  <span class="code-comment">// Gin Panic 恢复中间件</span>
    log.Println("Gin 引擎初始化完成...")

    <span class="code-comment">// --- 5. 定义 API 路由 ---</span>
    log.Println("正在定义 API 路由...")
    userRoutes := router.Group("/users")
    {
        userRoutes.POST("", createUserHandler)
        userRoutes.GET("/:id", getUserHandler)
    }
    router.GET("/health", func(c *gin.Context) { /* ... */ }) <span class="code-comment">// 健康检查路由</span>
    log.Println("API 路由定义完成。")

    <span class="code-comment">// --- 6. 配置并启动 HTTP 服务器 ---</span>
    log.Println("正在配置 HTTP 服务器...")
    server := &http.Server{
        Addr:    ":8080",        <span class="code-comment">// 监听地址和端口</span>
        Handler: router,         <span class="code-comment">// 使用 Gin 引擎作为处理器</span>
        ReadTimeout:  15 * time.Second,
        WriteTimeout: 15 * time.Second,
        IdleTimeout:  60 * time.Second,
    }

    <span class="code-comment">// 在一个新的 goroutine 中启动服务器，避免阻塞主线程</span>
    go func() {
        log.Printf("HTTP 服务器正在启动，监听地址 %s", server.Addr)
        // ... 打印启动信息 ...
        if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
            <span class="code-comment">// 如果不是服务器正常关闭导致的错误，则认为是致命错误</span>
            log.Fatalf("致命错误: HTTP 服务器启动失败: %v", err)
        }
        log.Println("HTTP 服务器监听协程已正常退出。")
    }()

    <span class="code-comment">// --- 7. 处理优雅停机 (Graceful Shutdown) ---</span>
    log.Println("设置优雅停机信号监听 (SIGINT, SIGTERM)...")
    quit := make(chan os.Signal, 1) <span class="code-comment">// 创建信号通道</span>
    <span class="code-comment">// 监听中断信号 (Ctrl+C) 和终止信号 (kill 命令)</span>
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    sig := <-quit <span class="code-comment">// 阻塞，直到接收到信号</span>
    log.Printf("接收到信号 %q，开始执行优雅停机...", sig)

    <span class="code-comment">// 创建带超时的上下文，用于控制关闭过程</span>
    ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
    defer cancel() <span class="code-comment">// 确保上下文资源被释放</span>

    log.Println("正在优雅地关闭 HTTP 服务器...")
    <span class="code-comment">// 调用服务器的 Shutdown 方法</span>
    if err := server.Shutdown(ctx); err != nil {
        log.Printf("错误: HTTP 服务器优雅停机失败: %v", err)
    } else {
        log.Println("HTTP 服务器已优雅关闭。")
    }

    <span class="code-comment">// main 函数即将结束，之前 defer 的 Redis 关闭和 OTel 关闭函数会被执行</span>
    log.Println("应用程序即将退出。")
} // main 函数结束
</code></pre>
    <h3>流程图:</h3>
    <div class="diagram">
        <svg width="400" height="950" xmlns="http://www.w3.org/2000/svg">
            <defs><marker id="arrowhead" markerWidth="10" markerHeight="7" refX="0" refY="3.5" orient="auto"><polygon points="0 0, 10 3.5, 0 7" fill="#6c757d" /></marker></defs>

            <rect x="100" y="20" width="200" height="40" class="svg-rect"/>
            <text x="200" y="40" class="svg-text">程序启动 (main)</text>

            <rect x="100" y="90" width="200" height="50" class="svg-rect"/>
            <text x="200" y="110" class="svg-text">1. 初始化 OTel</text>
            <text x="200" y="125" class="svg-text" style="font-size:10px;">(设置全局 Provider/Propagator)</text>
            <text x="200" y="138" class="svg-text" style="font-size:10px;">(defer OTel Shutdown)</text>

            <rect x="100" y="170" width="200" height="50" class="svg-rect"/>
            <text x="200" y="190" class="svg-text">2. 初始化数据库 (GORM)</text>
            <text x="200" y="205" class="svg-text" style="font-size:10px;">(连接, otelgorm, 迁移)</text>

            <rect x="100" y="250" width="200" height="50" class="svg-rect"/>
            <text x="200" y="270" class="svg-text">3. 初始化 Redis</text>
            <text x="200" y="285" class="svg-text" style="font-size:10px;">(连接, redisotel)</text>
            <text x="200" y="298" class="svg-text" style="font-size:10px;">(defer Redis Close)</text>


            <rect x="100" y="330" width="200" height="50" class="svg-rect"/>
            <text x="200" y="350" class="svg-text">4. 初始化 Gin 引擎</text>
            <text x="200" y="365" class="svg-text" style="font-size:10px;">(添加 otelgin 等中间件)</text>

            <rect x="100" y="410" width="200" height="40" class="svg-rect"/>
            <text x="200" y="430" class="svg-text">5. 定义 API 路由</text>

            <rect x="100" y="480" width="200" height="50" class="svg-rect"/>
            <text x="200" y="500" class="svg-text">6. 配置 HTTP 服务器</text>
            <text x="200" y="515" class="svg-text" style="font-size:10px;">(端口, Handler, 超时)</text>

            <rect x="100" y="560" width="200" height="50" class="svg-rect"/>
            <text x="200" y="580" class="svg-text">启动 HTTP 服务器</text>
            <text x="200" y="595" class="svg-text" style="font-size:10px;">(在新的 Goroutine 中)</text>

            <rect x="100" y="640" width="200" height="50" class="svg-rect"/>
            <text x="200" y="660" class="svg-text">7. 监听系统信号</text>
            <text x="200" y="675" class="svg-text" style="font-size:10px;">(SIGINT, SIGTERM)</text>

            <rect x="100" y="720" width="200" height="40" class="svg-rect"/>
            <text x="200" y="740" class="svg-text">阻塞，等待信号...</text>

            <rect x="100" y="790" width="200" height="40" class="svg-rect"/>
            <text x="200" y="810" class="svg-text">接收到信号!</text>

            <rect x="100" y="860" width="200" height="50" class="svg-rect"/>
            <text x="200" y="880" class="svg-text">调用 server.Shutdown()</text>
            <text x="200" y="895" class="svg-text" style="font-size:10px;">(带超时 Context)</text>

            <rect x="100" y="940" width="200" height="40" class="svg-rect"/>
            <text x="200" y="960" class="svg-text">执行 defer (关闭 Redis, OTel)</text>

            <rect x="100" y="1010" width="200" height="40" class="svg-rect"/>
            <text x="200" y="1030" class="svg-text">程序退出</text>


            <!-- Arrows -->
            <line x1="200" y1="60" x2="200" y2="90" class="svg-arrow"/>
            <line x1="200" y1="140" x2="200" y2="170" class="svg-arrow"/>
            <line x1="200" y1="220" x2="200" y2="250" class="svg-arrow"/>
            <line x1="200" y1="300" x2="200" y2="330" class="svg-arrow"/>
            <line x1="200" y1="380" x2="200" y2="410" class="svg-arrow"/>
            <line x1="200" y1="450" x2="200" y2="480" class="svg-arrow"/>
            <line x1="200" y1="530" x2="200" y2="560" class="svg-arrow"/>
            <line x1="200" y1="610" x2="200" y2="640" class="svg-arrow"/>
            <line x1="200" y1="690" x2="200" y2="720" class="svg-arrow"/>
            <line x1="200" y1="760" x2="200" y2="790" class="svg-arrow"/> <text x="230" y="775" class="svg-label">收到信号</text>
            <line x1="200" y1="830" x2="200" y2="860" class="svg-arrow"/>
            <line x1="200" y1="910" x2="200" y2="940" class="svg-arrow"/>
            <line x1="200" y1="980" x2="200" y2="1010" class="svg-arrow"/>

        </svg>
    </div>
    <h3>解释:</h3>
    <ol>
        <li><strong>初始化依赖:</strong> 依次调用前面定义的 <code>initTracerProvider</code>, <code>initDB</code>, <code>initRedis</code> 函数。任何初始化失败都会导致程序终止 (<code>log.Fatalf</code>)。注意，OTel 和 Redis 的关闭函数通过 <code>defer</code> 注册，确保它们在 <code>main</code> 函数结束前被调用。</li>
        <li><strong>初始化 Gin:</strong> 创建 Gin 引擎实例 (<code>gin.New()</code>)。重要的是，<span class="highlight">先添加 <code>otelgin.Middleware</code></span>，然后再添加其他中间件（如 Logger, Recovery）。这样可以确保 OTel 中间件能够追踪到尽可能多的请求处理时间和后续中间件的操作。</li>
        <li><strong>定义路由:</strong> 设置 URL 路径（如 <code>/users</code>, <code>/users/:id</code>, <code>/health</code>）与对应的处理器函数（如 <code>createUserHandler</code>, <code>getUserHandler</code>）的映射关系。</li>
        <li><strong>配置 HTTP 服务器:</strong> 创建一个标准库 <code>http.Server</code> 实例，配置监听地址 (<code>:8080</code> 表示监听所有网络接口的 8080 端口) 和请求处理器（使用 Gin router）。还配置了读、写和空闲超时，增强服务的稳定性。</li>
        <li><strong>启动服务器 (Goroutine):</strong> 使用 <code>go func() { ... }()</code> 在一个新的 <a href="glossary.html#goroutine">Goroutine</a>（轻量级线程）中调用 <code>server.ListenAndServe()</code> 来启动 HTTP 服务器。这样做是为了<span class="highlight">不阻塞主 Goroutine</span>，主 Goroutine 需要继续执行后续的信号监听逻辑。</li>
        <li><strong>监听信号:</strong> 创建一个通道 <code>quit</code>，并使用 <code>signal.Notify</code> 监听操作系统的 <code>SIGINT</code> (通常由 Ctrl+C 触发) 和 <code>SIGTERM</code> (通常由系统或容器编排工具发送的终止信号) 。<code>sig := <-quit</code> 会<span class="highlight">阻塞主 Goroutine</span>，直到接收到这两个信号中的任意一个。</li>
        <li><strong>执行优雅关闭:</strong>
            <ul>
                <li>一旦收到信号，程序打印日志，表明开始关闭。</li>
                <li>创建一个带有超时的 <a href="glossary.html#context"><code>context.Context</code></a> (例如 25 秒)。这个超时是为了给正在处理的请求以及后续的关闭步骤（包括 OTel 和 Redis 的关闭）设定一个总的时间限制。</li>
                <li>调用 <code>server.Shutdown(ctx)</code>。这个方法会：
                    <ol>
                        <li>立即停止接受新的 HTTP 连接。</li>
                        <li>等待当前正在处理的请求完成。</li>
                        <li>如果在超时时间内所有请求都处理完了，则正常关闭。</li>
                        <li>如果超时时间到了仍有请求未完成，则强制关闭剩余连接。</li>
                    </ol>
                </li>
                <li><code>ListenAndServe</code> 在 <code>Shutdown</code> 被调用后会返回 <code>http.ErrServerClosed</code> 错误，表示服务器是正常关闭的，我们的 Goroutine 也随之退出。</li>
            </ul>
        </li>
        <li><strong>执行 Defer:</strong> 当 <code>main</code> 函数即将退出时，之前注册的 <code>defer</code> 语句会<span class="highlight">按后进先出 (LIFO) 的顺序执行</span>。这意味着会先调用 Redis 的 <code>Close()</code>，然后调用 OTel 的 <code>Shutdown()</code>。</li>
        <li><strong>程序退出:</strong> 所有清理工作完成后，程序终止。</li>
    </ol>
</section>

</body>
</html>